module(...,package.seeall)

--special key that means anynode will do
any = {}
kinds = {}

function validassociationkind(kind)
  if (kind ~= "") or (kind == "*") then
    if type(kind) == "string" then
      if kinds[kind] then
        return true
      end
    end  
  end
  return false
end


add = function(source,kind,target)
  assert(source~=nil,"attempt to link to a nil source")
  assert(target~=nil,"attempt to link to a nil target")
  assert(source~=target,"attempt to create a cyclic association for "..tostring(source))
  assert(validassociationkind(kind),"attempt to create an invalid kind of association "..tostring(kind))
  --make sure the associations table exists
  associations = associations or weaktable({all={},source={},target={},kind={}})
  local association={source=source,kind=kind,target=target}
  associations.all[association]=association
  associations.source[source]=associations.source[source] or weaktable() 
  associations.source[source][association]=association
  associations.target[target]=associations.target[target] or weaktable()
  associations.target[target][association]=association
  associations.kind[kind]=associations.kind[kind] or weaktable()
  associations.kind[kind][association]=association
  --associate source
  source.associations.associations[association]=association
  source.associations.__addassociation(association)
  --associate target  
  target.associations.associations[association]=association
  target.associations.__addassociation(association)
end

removeassociation = function(association)
  associations.all[association]=nil
  local source,target,kind = association.source,association.target,association.kind
  if source ~= nil then
    source.associations.__removeassociation(association)
    source.associations.associations[association]=nil
    associations.source[source][association]=nil
  end
  if target ~= nil then
    target.associations.__removeassociation(association)  
    target.associations.associations[association]=nil
    associations.target[target][association]=nil
  end
  associations.kind[kind][association]=nil    
end

local associationsystem_mt= {
   associations={},
   __addassociation=function(a) 
     --empty call back
     --should we produce an event?
   end,
   __removeassociation=function(a) 
     --empty call back
     --should we produce an event?
   end,
   getassociatedtargets = function(self,kind) 
     local nodes ={}
     local a,n
     for a in pairs(self.associations) do
       if a.kind == kind then
         n = a.target
         if n == nil then 
           printd("nil target found")
           removeassociation(a)
         elseif n ~= self then
           nodes[n]=kind
         else
           --this association has me as target
         end
       end
     end
     return nodes     
   end,
   getassociatedsources = function(self,kind) 
     local nodes ={}
     local a,n
     for a in pairs(self.associations) do
       if a.kind == kind then
         n = a.source
         if n == nil then 
           printd("nil target found")
           removeassociation(a)         
         elseif n ~= self then
           nodes[n]=kind
         else
           --this association has me as source
         end
       end
     end
     return nodes     
   end,
   getassociatednodes = function(self,kind) 
     local nodes ={}
     local a,n
     for a in pairs(self.associations) do
       if a.kind == kind then
         n = a.target
         if n == nil then
           printd("nil target found")
           removeassociation(a)         
         else
           if n ~= self then
             nodes[n]=kind
           end
           n = a.source
           if n ~= self then
             nodes[n]=kind
           end
         end
       end
     end
     return nodes
   end,
   foreachassociation = function(self,func)
     local a
     for a in pairs(self.associations) do
       func(a)
     end
   end,
   __index = function(t,k)
     return getmetatable(t)[k]
   end
}

function newsystem(associations)
  local as = {}
  as.associations = associations or {}
  setmetatable(as,associationsystem_mt)
  return as  
end


local function testassociation(source,kind,target,a)
  if (source ~= any) and ((a.source ~= source) and (tonumber(a.source.__guid)~=tonumber(source))) then
    return false
  end
  if (target ~= any) and ((a.target ~= target) and (tonumber(a.target.__guid)~=tonumber(target))) then
    return false
  end
  if (kind ~= "") and (not(compare(a.kind,kind))) then
    return false  
  end
  return true
end

local function testassociationlist(source,kind,target,list)
  if list==nil then 
    return {}
  end
  local matches ={}
  for a in pairs(list) do
    if testassociation(source,kind,target,a) then
      table.insert(matches,a)
    end
  end  
  --print(#matches)
  return matches
end

find = function(source,kind,target)
  local matches = {}
  --if #matches == 0 then
    matches = testassociationlist(source,kind,target,associations.source[source])
  --end
  if #matches == 0 then
    matches = testassociationlist(source,kind,target,associations.target[target])
  end
  if #matches == 0 then
    matches = testassociationlist(source,kind,target,associations.kind[kind])
  end 
  if (#matches == 0) and (source == any) and (target == any) and (kind == "*") then
    log("association test has made a brute force search %s",debug.func.location.prev())
    matches = testassociationlist(source,kind,target,associations.all)
  end   
  return matches
end
